package uts.sdk.modules.kuxHtmlparser

import io.dcloud.uts.*
import io.dcloud.uts.Set as UTSSet

class HTMLParser(
	private val html: String, 
	private val handler: Handler,
	private var config: ParserConfig = ParserConfig()
) : IHTMLParser {
	private companion object {
		private val blockSet = UTSSet<String>().apply {
			add("div")
			add("p")
			add("h1")
			add("h2")
			add("h3")
			add("h4")
			add("h5")
			add("h6")
		}
		private val inlineSet = UTSSet<String>().apply {
			add("span")
			add("em")
			add("strong")
		}
		private val closeSelfSet = UTSSet<String>().apply {
			add("img")
			add("br")
			add("hr")
		}
		private val specialSet = UTSSet<String>().apply {
			add("script")
			add("style")
			add("textarea")
		}
		private val emptySet = UTSSet<String>().apply {
			add("meta")
			add("link")
			add("input")
		}
		private val DEFAULT_CONFIG = ParserConfig(
			blockElements = blockSet,
			inlineElements = inlineSet,
			closeSelfElements = closeSelfSet,
			specialElements = specialSet,
			emptyElements = emptySet
		)
		
		private val startTag = Regex("^<([-\\w]+)([^>]*)>")
		private val endTag = Regex("^</([-\\w]+)[^>]*>")
		private val attr = Regex("""([\w:-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^>\s]+))|([\w:-]+)""")
		private val comment = Regex("^<!--(.*?)-->")
		private val codeTag = Regex("^<code.*?>(.*?)</code>", RegexOption.DOT_MATCHES_ALL)
		
		private val empty = makeSet("area,base,basefont,br,col,frame,hr,img,input,link,meta,param,embed,command,keygen,source,track,wbr")
		private val block = makeSet("a,address,code,article,applet,aside,audio,blockquote,button,canvas,center,dd,del,dir,div,dl,dt,fieldset,figcaption,figure,footer,form,frameset,h1,h2,h3,h4,h5,h6,header,hgroup,hr,iframe,ins,isindex,li,map,menu,noframes,noscript,object,ol,output,p,pre,section,script,table,tbody,td,tfoot,th,thead,tr,ul,video")
		private val inline = makeSet("abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,textarea,tt,u,var")
		private val closeSelf = makeSet("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr")
		private val special = makeSet("wxxxcode-style,script,style,view,scroll-view,block")
		
		private fun makeSet(items: String) = items.split(',').map { it.trim() }.toSet()
		
		private fun mergeConfigs(base: ParserConfig, custom: ParserConfig): ParserConfig {
			return ParserConfig(
				blockElements = custom.blockElements ?: base.blockElements,
				inlineElements = custom.inlineElements ?: base.inlineElements,
				closeSelfElements = custom.closeSelfElements ?: base.closeSelfElements,
				specialElements = custom.specialElements ?: base.specialElements,
				emptyElements = custom.emptyElements ?: base.emptyElements
			)
		}
	}
	
	private val stack = mutableListOf<String>()
	private var remaining = html
	private var codeContent: String? = null
	
	init {
		config = mergeConfigs(DEFAULT_CONFIG, config)
	}
	
	override fun configure(newConfig: ParserConfig): IHTMLParser {
		return HTMLParser(
			html = this.html,
			handler = this.handler,
			config = mergeConfigs(this.config, newConfig)
		)
	}
	
	override fun parse() {
		while (remaining.isNotEmpty()) {
			if (!currentTagIsSpecial()) {
				parseNormalContent()
			} else {
				parseSpecialContent()
			}
		}
		closeRemainingTags()
	}
	
	private fun currentTagIsSpecial(): Boolean {
		return stack.isNotEmpty() && config.specialElements!!.contains(stack.last())
	}
	
	private fun parseNormalContent() {
		when {
			tryParseComment() -> return
			tryParseEndTag() -> return
			tryParseStartTag() -> return
			else -> parseText()
		}
	}
	
	private fun tryParseComment(): Boolean {
		val match = comment.find(remaining) ?: return false
		handler.comment?.invoke(match.groupValues[1])
		remaining = remaining.substring(match.range.last + 1)
		return true
	}
	
	private fun tryParseEndTag(): Boolean {
		val match = endTag.find(remaining) ?: return false
		val tagName = match.groupValues[1].lowercase()
		val closingPos = stack.lastIndexOf(tagName)
		
		if (closingPos >= 0) {
			(stack.size - 1 downTo closingPos).forEach { 
				handler.end?.invoke(stack[it])
			}
			stack.subList(closingPos, stack.size).clear()
		}
		remaining = remaining.substring(match.range.last + 1)
		return true
	}
	
	private fun tryParseStartTag(): Boolean {
		val match = startTag.find(remaining) ?: return false
		val (tag, attributes) = match.destructured
		val tagName = tag.lowercase()
		// val isUnary = empty.contains(tagName) || match.value.endsWith("/>")
		val isUnary = config.emptyElements!!.contains(tagName) || match.value.endsWith("/>")
		
		processNestingRules(tagName)
		handleAttributes(tagName, attributes, isUnary)
		
		if (!isUnary) stack.add(tagName)
		remaining = remaining.substring(match.range.last + 1)
		return true
	}	
	
	private fun processNestingRules(tagName: String) {
		if (config.blockElements!!.contains(tagName)) {
			// while (stack.lastOrNull()?.let { inline.contains(it) } == true) {
			// 	closeLastTag()
			// }
			while (stack.isNotEmpty() && config.inlineElements!!.contains(stack.last())) {
				closeLastTag()
			}
		}
		// if (closeSelf.contains(tagName) && stack.lastOrNull() == tagName) {
		// 	closeLastTag()
		// }
		if (config.closeSelfElements!!.contains(tagName) && stack.lastOrNull() == tagName) {
			closeLastTag()
		}
	}
	
	private fun handleAttributes(tagName: String, attrStr: String, unary: Boolean) {
		val attrs = UTSArray<Attr>()
		var matchResult = attr.find(attrStr)
		
		while (matchResult != null) {
			val groups = matchResult.groupValues.drop(1).filterNot { it.isEmpty() }
			when {
				groups.size >= 5 && groups[4].isNotEmpty() -> 
				    attrs.push(Attr(groups[4], ""))
				groups.size >= 2 -> {
					val value = groups[1].ifEmpty { 
						groups.getOrNull(2) ?: groups.getOrNull(3) ?: ""
					}
					attrs.push(Attr(groups[0], value))
				}
			}
			matchResult = matchResult.next()
		}
		
		if (tagName == "code") {
			codeContent = codeTag.find(remaining)?.groupValues?.get(1)
		}
		
		handler.start?.invoke(tagName, attrs, unary, codeContent)
		codeContent = null
	}
	
	private fun parseText() {
		val nextLT = remaining.indexOf('<').takeIf { it != -1 } ?: remaining.length
		val text = remaining.substring(0, nextLT).trim()
		if (text.isNotEmpty()) handler.chars?.invoke(text)
		remaining = remaining.substring(nextLT)
	}
	
	private fun parseSpecialContent() {
		val tag = stack.last()
		val endPattern = "</$tag\\b[^>]*>".toRegex(RegexOption.IGNORE_CASE)
		val match = endPattern.find(remaining)
		
		if (match != null) {
			val content = remaining.substring(0, match.range.first)
				.replace(Regex("<!--.*?-->|<!\\[CDATA\\[.*?]]>"), "")
			handler.chars?.invoke(content)
			remaining = remaining.substring(match.range.last + 1)
			closeLastTag()
		} else {
			handler.chars?.invoke(remaining)
			remaining = ""
		}
	}
	
	private fun closeLastTag() {
		if (stack.isNotEmpty()) {
			handler.end?.invoke(stack.removeAt(stack.size - 1))
		}
	}
	
	private fun closeRemainingTags() {
		while (stack.isNotEmpty()) {
			closeLastTag()
		}
	}
	
	class ParseException(message: String) : Exception(message)
}